package com.platform.toutiao.api;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;

import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.log4j.Logger;

import com.google.common.base.Joiner;
import com.platform.toutiao.bean.TouTiaoAccessToken;
import com.platform.toutiao.bean.TouTiaoCode2SessionResult;
import com.platform.toutiao.bean.TouTiaoUserInfo;
import com.platform.toutiao.config.TouTiaoApiUrl;
import com.platform.toutiao.config.TouTiaoConfig;
import com.platform.toutiao.error.TouTiaoError;
import com.platform.toutiao.error.TouTiaoErrorException;
import com.platform.toutiao.util.http.RequestExecutor;
import com.platform.toutiao.util.http.SimpleGetRequestExecutor;

import me.chanjar.weixin.common.util.DataUtils;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;



import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;






public class TouTiaoServiceHttpClient implements RequestHttp<CloseableHttpClient, HttpHost> {
    private Logger log = Logger.getLogger(getClass());
	private CloseableHttpClient httpClient;
	private TouTiaoConfig config;
	private HttpHost httpProxy;

	private int retrySleepMillis = 1000;
	private int maxRetryTimes = 5;

	public TouTiaoServiceHttpClient(TouTiaoConfig config) {
		this.config = config;
		initHttp();
	}
	
	public CloseableHttpClient getRequestHttpClient() {
		return httpClient;
	}

	public void initHttp() {
		ApacheHttpClientBuilder apacheHttpClientBuilder = config.getApacheHttpClientBuilder();
		if (null == apacheHttpClientBuilder) {
			apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
		}

		apacheHttpClientBuilder.httpProxyHost(config.getHttpProxyHost()).httpProxyPort(config.getHttpProxyPort())
				.httpProxyUsername(config.getHttpProxyUsername()).httpProxyPassword(config.getHttpProxyPassword());

		if (config.getHttpProxyHost() != null && config.getHttpProxyPort() > 0) {
			this.httpProxy = new HttpHost(config.getHttpProxyHost(), config.getHttpProxyPort());
		}

		this.httpClient = apacheHttpClientBuilder.build();
	}

	/**
	 * @param 
	 * @return {"access_token":"123","expires_in":7200}
	 * @throws TouTiaoErrorException
	 */
	public String getAccessToken(boolean forceRefresh) throws TouTiaoErrorException {
		if (!config.isAccessTokenExpired() && !forceRefresh) {
			return config.getAccessToken();
		}
		Lock lock = config.getAccessTokenLock();
		lock.lock();
		try {
			if (!config.isAccessTokenExpired() && !forceRefresh) {
				return config.getAccessToken();
			}
			String url = String.format(TouTiaoApiUrl.GET_ACCESS_TOKEN_URL, config.getAppId(), config.getSecret());
			try {
				HttpGet httpGet = new HttpGet(url);
				if (this.httpProxy != null) {
					RequestConfig requestConfig = RequestConfig.custom().setProxy(this.httpProxy).build();
					httpGet.setConfig(requestConfig);
				}
				try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
					return this.extractAccessToken(new BasicResponseHandler().handleResponse(response));
				} catch (TouTiaoErrorException e) {
					throw e;
				}finally {
					httpGet.releaseConnection();
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		} finally {
			lock.unlock();
		}
	}
	
	public TouTiaoCode2SessionResult code2Session(String code) throws TouTiaoErrorException{
	    Map<String, String> params = new HashMap<>(8);
	    params.put("appid", config.getAppId());
	    params.put("secret", config.getSecret());
	    params.put("code", code);
//		String url = String.format(TouTiaoApiUrl.CODE_2_SESSION, config.getAppId(), config.getSecret());
	    log.info(TouTiaoApiUrl.CODE_2_SESSION + "?" +Joiner.on("&").withKeyValueSeparator("=").join(params));
	    String result = get(TouTiaoApiUrl.CODE_2_SESSION, Joiner.on("&").withKeyValueSeparator("=").join(params));
	    return TouTiaoCode2SessionResult.fromJson(result);
	}
	
	public String get(String url, String queryParam) throws TouTiaoErrorException {
	    return execute(SimpleGetRequestExecutor.create(this), url, queryParam,false);
	}

	protected String extractAccessToken(String resultContent) throws TouTiaoErrorException {
		TouTiaoAccessToken accessToken = TouTiaoAccessToken.fromJson(resultContent);
		config.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
		return config.getAccessToken();
	}
	
	public String get(String url, String queryParam, boolean isAddAccessToken) throws TouTiaoErrorException {
	    return execute(SimpleGetRequestExecutor.create(this), url, queryParam,isAddAccessToken);
	}
	  /**
	   * 向微信端发送请求，在这里执行的策略是当发生access_token过期时才去刷新，然后重新执行请求，而不是全局定时请求
	   */
	public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data, boolean isAddAccessToken)
			throws TouTiaoErrorException {
		int retryTimes = 0;
		do {
			try {
				return this.executeInternal(executor, uri, data, isAddAccessToken);
			} catch (TouTiaoErrorException e) {
				TouTiaoError error = e.getError();
				if (retryTimes + 1 > this.maxRetryTimes) {
					log.warn(String.format("重试达到最大次数【%d】", maxRetryTimes));
					error.setErrmsg(error.getErrmsg() + "头条服务端异常，超出重试次数！");
					// 最后一次重试失败后，直接抛出异常，不再等待
					throw new TouTiaoErrorException(error);
				}

				// -1 系统繁忙, 1000ms后重试
				if (error.getErrcode() == -1) {
					int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
					try {
						log.warn(String.format("头条系统繁忙，%d ms 后重试(第%d次)", sleepMillis, retryTimes + 1));
						Thread.sleep(sleepMillis);
					} catch (InterruptedException e1) {
						Thread.currentThread().interrupt();
					}
				} else {
					throw e;
				}
			}
		} while (retryTimes++ < this.maxRetryTimes);

		log.warn(String.format("重试达到最大次数【%d】", this.maxRetryTimes));
		throw new RuntimeException("头条服务端异常，超出重试次数");
	}

	private <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data,boolean isAddAccessToken) throws TouTiaoErrorException {
		E dataForLog = DataUtils.handleDataWithSecret(data);

		if (uri.contains("access_token=")) {
			throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
		}
		if(isAddAccessToken) {
			String accessToken = getAccessToken(false);
			uri = uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
		}
		try {
			T result = executor.execute(uri, data);
			log.info(String.format("\n【请求地址】: %s\n【请求参数】：%s\n【响应数据】：%s", uri, dataForLog, result));
			log.debug(String.format("\n【请求地址】: %s\n【请求参数】：%s\n【响应数据】：%s", uri, dataForLog, result));
			return result;
		} catch (TouTiaoErrorException e) {
			TouTiaoError error = e.getError();

			if (error.getErrcode() != 0) {
				log.error(String.format("\n【请求地址】: %s\n【请求参数】：%s\n【错误信息】：%s", uri, dataForLog, error));
				throw new TouTiaoErrorException(error, e);
			}
			return null;
		} catch (IOException e) {
			log.error(String.format("\n【请求地址】: %s\n【请求参数】：%s\n【异常信息】：%s", uri, dataForLog, e.getMessage()));
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * a解密用户信息
	 * @param signature
	 * @param encryptedData
	 * @param iv
	 * @return
	 * @throws TouTiaoErrorException
	 */
	public TouTiaoUserInfo getUserInfo(String encryptedData,String sessionKey,String iv) throws TouTiaoErrorException{
		try {
			String userInfoStr = decrypt(encryptedData,sessionKey,iv);
			TouTiaoUserInfo userInfo = TouTiaoUserInfo.fromJson(userInfoStr);
			return userInfo;
		} catch (InvalidKeyException e) {
			log.error(e.getMessage(),e);
			throw new TouTiaoErrorException(e);
		} catch (NoSuchPaddingException e) {
			log.error(e.getMessage(),e);
			throw new TouTiaoErrorException(e);
		} catch (NoSuchAlgorithmException e) {
			log.error(e.getMessage(),e);
			throw new TouTiaoErrorException(e);
		} catch (InvalidAlgorithmParameterException e) {
			log.error(e.getMessage(),e);
			throw new TouTiaoErrorException(e);
		} catch (BadPaddingException e) {
			log.error(e.getMessage(),e);
			throw new TouTiaoErrorException(e);
		} catch (IllegalBlockSizeException e) {
			log.error(e.getMessage(),e);
			throw new TouTiaoErrorException(e);
		}
	}
	
	public static String decrypt(String encryptedData, String sessionKey, String iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] sessionKeyBytes = decoder.decode(sessionKey);
        byte[] ivBytes = decoder.decode(iv);
        byte[] encryptedBytes = decoder.decode(encryptedData);

        // JDK does not support PKCS7Padding, use PKCS5Padding instead
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec skeySpec = new SecretKeySpec(sessionKeyBytes, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);
        byte[] ret = cipher.doFinal(encryptedBytes);
        return new String(ret);
    }
	
	@Override
	public HttpHost getRequestHttpProxy() {
		return httpProxy;
	}
	@Override
	public HttpType getRequestType() {
		return HttpType.APACHE_HTTP;
	}
}
